/*
 * Clipboard.c : handle interaction with clipboard
 *
 * Written by T.Pierron, Mar 2009.
 */


#include "SIT_P.h"
#include "graphics.h"

static ListHead mHGlobal; /* ClipHGlobal */

/* Simple function to copy block of UTF-8 text to clipboard */
DLLIMP Bool SIT_CopyToClipboard(SIT_Widget w, STRPTR text)
{
	LPWSTR mem;
	int len = MultiByteToWideChar(CP_UTF8, 0, text, -1, NULL, 0);
	HGLOBAL hmem = GlobalAlloc(GMEM_MOVEABLE | GMEM_SHARE, len * sizeof *mem);

	if (hmem)
	{
		mem = GlobalLock(hmem);

		MultiByteToWideChar(CP_UTF8, 0, text, -1, mem, len);

		GlobalUnlock(hmem);

		if (OpenClipboard(w->sw_Handle))
		{
			EmptyClipboard();
			SetClipboardData(CF_UNICODETEXT, hmem);
			CloseClipboard();
			return True;
		}
		else GlobalFree(hmem);
	}
	return False;
}

typedef struct { HGLOBAL hmem; uint32_t usage; } * TxtStream;
#define SZTXT  sizeof (*(TxtStream)0)

DLLIMP Bool SIT_SetClipboardData(SIT_Widget w, ...)
{
	STRPTR  type;
	int     ret  = 0;
	int     isrender = (w->sw_Flags & SITF_Internal) > 0;
	va_list args;
	va_start(args, w);

	if (isrender == 0)
		isrender = OpenClipboard(w->sw_Handle) && EmptyClipboard();
	else
		isrender = 2;

	if (isrender)
	{
		while ((type = va_arg(args, STRPTR)))
		{
			HGLOBAL hmem = NULL;
			APTR    mem  = va_arg(args, APTR);
			int     fmt  = 0;

			if (strcasecmp(type, "TEXT")     == 0) fmt = CF_UNICODETEXT; else
			if (strcasecmp(type, "TEXTANSI") == 0) fmt = CF_TEXT; else
			if (strcasecmp(type, "BITMAP")   == 0) fmt = CF_DIB;

			if (mem == NULL) /* Delayed rendering */
			{
				if (isrender == 2) return False; /* Useless to give NULL at this point */
				SetClipboardData(fmt, NULL);
				ret = 1;
			}
			else switch (fmt) {
			case CF_UNICODETEXT:
			{	ULONG size = ((TxtStream)mem)->usage;

				hmem = ((TxtStream)mem)->hmem;
				memmove(mem, mem+MAX(size,SZTXT), MIN(size,SZTXT));
				* (LPWSTR) (mem+size) = 0;
				GlobalUnlock(hmem);

				ret = SetClipboardData(CF_UNICODETEXT, hmem) ? 1 : 2; /* Now hmem is property of the system */
			}	break;
			case CF_DIB:
			{	struct Image_t buf;
				Image        img = mem;
				LPBITMAPINFO bi;

				/* Not alloced through SIT_AllocClipboardObject(), we need a duplicate :-/ */
				if ((img->encoder & GFX_GlobalAlloced) == 0) {
					if ((img = SIT_AllocClipboardObject("BITMAP", img->width, img->height, img->bpp)))
						memcpy(img->bitmap, ((Image)mem)->bitmap, img->stride * img->height);
					else
						{ ret = 2; GlobalUnlock(hmem); break; }
				}

				bi   = (APTR) img;
				buf  = *img;
				hmem = * (HGLOBAL *) (img+1);
				memset(bi, 0, sizeof (BITMAPINFOHEADER));
				// XXX Will contain garbage if GFX_Premultiplied is set
				bi->bmiHeader.biSize = sizeof bi->bmiHeader;
				bi->bmiHeader.biWidth = buf.width;
				bi->bmiHeader.biHeight = - buf.height;
				bi->bmiHeader.biPlanes = 1;
				bi->bmiHeader.biBitCount = buf.bpp;
				/* Palette already set */
				GlobalUnlock(hmem);

				ret = SetClipboardData(CF_DIB, hmem) ? 1 : 2;
			}	break;
			default: /* Custom format: register it on the fly */
			{	ClipHGlobal clip;
				for (clip = HEAD(mHGlobal); clip && clip->mem != mem; NEXT(clip));
				if (clip == NULL) { ret = 0; break; } /* Not found? */
				ListRemove(&mHGlobal, &clip->node);
				hmem = clip->hglob;
				free(clip);
				if (fmt != CF_TEXT)
				{
					LPWSTR fmtstr;
					UTF8ToUTF16(type, fmtstr);
					fmt = RegisterClipboardFormat(fmtstr);
				}
				GlobalUnlock(hmem);
				ret = fmt > 0 && SetClipboardData(fmt, hmem) ? 1 : 2;
			}}
			if (ret != 1 && hmem)
			{
				/* Fail to put data into clipboard: free resources */
				if (ret == 0) GlobalUnlock(hmem);
				GlobalFree(hmem);
				/* We still continue, because there is memory blocks allocated that still needs to be free()'ed */
			}
		}
		if (isrender < 2) CloseClipboard(); /* OnClipRender, must not close clipboard */
	}
	va_end(args);
	/* Unclaimed memory block? */
	while (mHGlobal.lh_Count) {
		ClipHGlobal clip = (APTR) ListRemHead(&mHGlobal);
		GlobalUnlock(clip->hglob);
		GlobalFree(clip->hglob);
		free(clip);
	}
	return ret;
}

DLLIMP int SIT_IsFormatAvailable(STRPTR type)
{
	STRPTR * fmt = SIT_GetClipboardFormats();
	int      i;

	if (fmt == NULL) return 0;
	if (type == NULL) type = "TEXT";
	for (i = 0; fmt[i] && strcasecmp(fmt[i], type); i ++);
	if (fmt[i]) return * (uint32_t *) (fmt[i]-4);
	return 0;
}

DLLIMP SIT_Widget SIT_GetClipboardOwner(void)
{
	HWND wnd = GetClipboardOwner();
	APTR ret = NULL;
	if (wnd)
	{
		/* Check that we own the window (ie: can access its address space) */
		DWORD procid;
		GetWindowThreadProcessId(wnd, &procid);
		if (procid == GetCurrentProcessId())
			ret = (APTR) GetProp(wnd, sit_class);
	}
	return ret;
}

STRPTR SIT_FormatName(int type)
{
	STRPTR * fmt = SIT_GetClipboardFormats();
	int      i;

	if (fmt == NULL) return NULL;
	for (i = 0; fmt[i] && * (uint32_t *) (fmt[i]-4) != type; i ++);
	return fmt[i];
}

DLLIMP APTR SIT_AllocClipboardObject(STRPTR type, ...)
{
	va_list args;
	int     sz;
	HGLOBAL hmem;
	APTR    ret;

	va_start(args, type);
	if (type && strcasecmp(type, "BITMAP") == 0)
	{
		int w = va_arg(args, int);
		int h = va_arg(args, int);
		int b = va_arg(args, int);
		int s = ((w * b>>3) + 3) & ~3;
		int hdr = sizeof (BITMAPINFOHEADER);
		Image i;

		if (b <= 8) hdr += (1 << b) * sizeof (RGBQUAD);
		sz = hdr + s * h;

		hmem = GlobalAlloc(GMEM_MOVEABLE | GMEM_SHARE | GMEM_ZEROINIT, sz);
		if (hmem == NULL) return NULL;
		i = ret = GlobalLock(hmem);
		if (i == NULL) { GlobalFree(hmem); return NULL; }
		i->width = w;
		i->height = h;
		i->bpp = b;
		i->stride = s;
		i->bitmap = ret + hdr;
		i->palette = (DATA8) ((LPBITMAPINFO)ret)->bmiColors;
		i->encoder = GFX_GlobalAlloced;
		* (HGLOBAL *) (i+1) = hmem;
	}
	else
	{
		int flg = 0;
		sz = (type == NULL || strcasecmp(type, "TEXT") == 0 ? flg = 256 : va_arg(args, int));
		hmem = GlobalAlloc(GMEM_MOVEABLE | GMEM_SHARE, sz);
		if (hmem == NULL) return NULL;
		ret = GlobalLock(hmem);
		if (ret == NULL) { GlobalFree(hmem); return NULL; }
		if (flg == 0)
		{
			/* Need to keep a link between HGLOBAL and mem. Cannot allocate more memory than requested. */
			ClipHGlobal clip = malloc(sizeof *clip);
			clip->hglob = hmem;
			clip->mem = ret;
			ListAddTail(&mHGlobal, &clip->node);
		}
		else
		{
			#define	stream      ((TxtStream)ret)
			stream->hmem = hmem;
			stream->usage = 0;
			#undef	stream
		}
	}
	va_end(args);
	return ret;
}

DLLIMP STRPTR SIT_AddTextToClipboard(STRPTR mem, STRPTR text, int sz)
{
	TxtStream txt = alloca(sizeof *txt);
	int len = MultiByteToWideChar(CP_UTF8, 0, text, sz, NULL, 0);
	int max, usage;

	if (mem == NULL) return NULL;

	*txt = * (TxtStream) mem; // Will be overwritten
	max  = GlobalSize(txt->hmem);

	if (txt->usage > 0)
		memcpy(mem, mem + MAX(txt->usage, SZTXT), MIN(txt->usage, SZTXT));

	if (sz < 0) len --; len <<= 1;
	usage = txt->usage + len + 2 + sizeof *txt;
	if (max < usage) {
		/* Round to nearest power of 2 for sz below 8Kb, then round to nearest 8Kb chunk */
		if (usage < 8192) {
			usage --;
			usage |= usage >> 1;
			usage |= usage >> 2;
			usage |= usage >> 4;
			usage |= usage >> 8; usage ++;
		}
		else usage = (usage+8191) & ~8191;
		/* Need to enlarge first */
		GlobalUnlock(txt->hmem);
		HGLOBAL niou = GlobalReAlloc(txt->hmem, usage, 0);
		if (niou == NULL) { GlobalFree(txt->hmem); return NULL; }
		mem = GlobalLock(niou); txt->hmem = niou;
	}
	/* Copy at the end */
	len = MultiByteToWideChar(CP_UTF8, 0, text, sz, (LPWSTR) (mem + txt->usage), (len>>1)+1);
	txt->usage += (len-(sz<0))<<1;
	memcpy(mem + MAX(txt->usage, SZTXT), mem, MIN(txt->usage, SZTXT));
	memcpy(mem, txt, SZTXT);
	return mem;
}

/* Check if clipboard contains text */
DLLIMP APTR SIT_GetFromClipboard(STRPTR type, int * size)
{
	extern HANDLE png;
	APTR ret = NULL;
	int  fmt = 0, alt = 0;

	/* Use png data if pnglib is loaded and data format is available */
	if (type == NULL || strcasecmp("BITMAP", type) || !png || (alt = SIT_IsFormatAvailable("image/png")) == 0)
		fmt = alt = SIT_IsFormatAvailable(type);
	else
		fmt = CF_DIB;

	if (fmt > 0 && OpenClipboard(NULL))
	{
		GLOBALHANDLE hmem = GetClipboardData(alt);

		if (hmem)
		{
			APTR mem = GlobalLock(hmem);
			int  len = GlobalSize(hmem);

			if (size) *size = len;

			switch (fmt) {
			case CF_UNICODETEXT: /* Transform to UTF-8 */
				len = WideCharToMultiByte(CP_UTF8, 0, mem, -1, NULL, 0, NULL, NULL);
				ret = malloc(len);
				if (ret)
					WideCharToMultiByte(CP_UTF8, 0, mem, -1, ret, len, NULL, NULL);
				break;

			case CF_DIB: /* Wrap into an Image structure */
				#define	bi  ((LPBITMAPINFOHEADER)mem)
				#define img ((Image)ret)
				if (alt == fmt)
				{
					DATA8 bits = (DATA8) (bi+1);
					int   b = bi->biBitCount;
					if (b <= 8) bits += ((1 << b)-1) * sizeof (RGBQUAD); // Skip color map
					ret = GFX_CreateImage(bi->biWidth, ABS(bi->biHeight), bi->biBitCount);
					if (ret == NULL) break;
					if (bi->biHeight > 0) /* bottom-up bitmap */
					{
						DATA8 d;
						for (b = img->height, d = img->bitmap + (img->height - 1) * img->stride; b > 0;
						     memcpy(d, bits, img->stride), d -= img->stride, bits += img->stride, b--);
					}
					else memcpy(img->bitmap, bits, img->stride * img->height);
				}
				else ret = GFX_LoadImageFromBuf(mem, len);

				#undef img
				#undef bi
				break;

			default: /* Duplicate whole stuff */
				ret = malloc(len);
				if (ret) memcpy(ret, mem, len);
			}
			GlobalUnlock(hmem);
		}
		CloseClipboard();
	}
	return ret;
}

/* Retrieve all clipboard formats in a NULL-terminated table */
STRPTR * SIT_GetClipboardFormats(void)
{
	WCHAR  format[80];
	STRPTR buf;
	int    nb, sz, len, fmt, mask;

	if ((nb = CountClipboardFormats()) == 0 || ! app) return NULL;
	if (app->sa_ClipSeq == (len = GetClipboardSequenceNumber())) return app->sa_Formats;
	app->sa_ClipSeq = len;
	OpenClipboard(NULL);
	fmt = EnumClipboardFormats(0);
	nb  = 1; sz = mask = 0;

	/* All of this, just to get clipboard format in 1 allocation (from SIT) :-/ */
	while (fmt)
	{
		switch (fmt) {
		case CF_BITMAP: case CF_DIB: case CF_DIBV5: if (mask&1) goto done; mask |= 1; len = 6; break;
		case CF_ENHMETAFILE: case CF_METAFILEPICT:  if (mask&2) goto done; mask |= 2; len = 6; break;
		case CF_OEMTEXT: case CF_TEXT: case CF_UNICODETEXT: if (mask&4) goto done; mask |= 4; len = 4; break;
		case CF_WAVE: len = 4; break;
		default:
			len = GetClipboardFormatName(fmt, format, DIM(format)); if (len == 0) goto done;
			len = WideCharToMultiByte(CP_UTF8, 0, format, len, NULL, 0, NULL, NULL);
		}
		nb ++; sz += 4 + ((len+4) & ~3);
		done: fmt = EnumClipboardFormats(fmt);
	}
	nb *= sizeof *app->sa_Formats;
	buf = realloc(app->sa_Formats, sz + nb); if (buf == NULL) return NULL;
	app->sa_Formats = (STRPTR *) buf;
	buf += nb + 4;

	fmt = EnumClipboardFormats(0);
	nb  = mask = 0;
	while (fmt)
	{
		STRPTR msg;
		app->sa_Formats[nb] = buf;
		switch (fmt) {
		case CF_BITMAP: case CF_DIB: case CF_DIBV5: if (mask&1) goto done2; mask|=1; msg = "BITMAP"; fmt = CF_DIB; break;
		case CF_ENHMETAFILE: case CF_METAFILEPICT:  if (mask&2) goto done2; mask|=2; msg = "VECTOR"; break;
		case CF_OEMTEXT: case CF_TEXT: case CF_UNICODETEXT: if (mask&4) goto done2; mask|=4; msg = "TEXT"; fmt = CF_UNICODETEXT; break;
		case CF_WAVE: msg = "WAVE"; break;
		default:
			msg = NULL;
			len = GetClipboardFormatName(fmt, format, DIM(format)); if (len == 0) goto done2;
			len = WideCharToMultiByte(CP_UTF8, 0, format, len, buf, sz, NULL, NULL);
			buf[len] = 0;
		}
		((uint32_t *)buf)[-1] = fmt; /* Yeah, a bit dodgy, but will prevent doing useless request later */
		if (msg) len = strlen(strcpy(buf, msg)); buf += 4 + ((len+4) & ~3); nb ++;
		done2: fmt = EnumClipboardFormats(fmt);
	}
	CloseClipboard();
	app->sa_Formats[nb] = NULL;
	return app->sa_Formats;
}
